Item 5: 了解 C++ 默默编写并调用哪些函数

编译器默认为一个类生成的函数

一个类:

  • 如果没有声明拷贝构造函数、拷贝赋值运算符和析构函数,则编译器会为类声明一个默认版本
  • 如果没有声明任何构造函数,编译器会为类声明一个default构造函数
  • 编译器生出的声明,都是public和inline的

当定义一个Empty类:

class Empty{};

就好像写下了这样的代码:

class Empty {
public:
  Empty() { ... }                  //@ default constructor
  Empty(const Empty& rhs) { ... }  //@ copy constructor
  ~Empty() { ... }   //@ destructor — see below,for whether it's virtual
  Empty& operator=(const Empty& rhs) { ... } //@ copy assignment operator
};

这些默认生出的函数只有在它们被调用的时候才会生成定义。

编译器默认生出的函数:

  • 析构函数:编译器生出的析构函数是非virtual的,除非它所在的类是从一个基类继承而来,而基类自己声明了一个虚拟析构函数,这种情况下,函数的虚拟性来自基类
  • 拷贝构造函数和拷贝赋值运算符:编译器生成版本只是简单地从源对象拷贝每一个非静态数据成员到目标对象,即浅拷贝。

编译器生成拷贝赋值运算符的相关约束

编译器生成的拷贝赋值运算符行为基本上与拷贝构造函数一致,但一般只有在生成的代码合法而且有适当机会证明它有意义时,编译器才会默认生成。万一两个条件有一个不符合,编译器会拒绝为 class 生成 operator=

template<class T>
class NamedObject {
public:
  NamedObject(std::string& name, const T& value);
  //@ assume no operator= is declared
  
private:
  std::string& nameValue;           //@ this is now a reference
  const T objectValue;              //@ this is now const
};

std::string newDog("Persephone");
std::string oldDog("Satch");
NamedObject<int> p(newDog, 2);                                
NamedObject<int> s(oldDog, 36);                                                
p = s;                                                                   

赋值之前,p.nameValue 和 s.nameValue 是不同的 string 对象的引用。如果赋值成功,则会修改引用绑定的对象,但是 C++ 并不允许“让 reference 改指向不同对象”。面对这种情况,编译器不知道该怎么办,C++的响应是拒绝编译那一行的赋值操作。如果你打算在一个“内含 reference 成员”的 class 内支持赋值操作,必须自己定义 copy assignment 操作符。

面对“内含 const 成员“的 class,编译器的反应也一样,更改 const 成员是不合法的,所以编译器不知道如何在它自己生成的赋值函数内面对它们。

如果某个基类将 copy assignment 操作符声明为 private ,编译器将拒绝其派生类生成一个 copy assignment 操作符,毕竟编译器为派生类生成的拷贝赋值运算符想象中可以处理基类成员,但实际上它无法调用派生类无权调用的成员函数。

需要注意的是,含有引用成员和 const 成员对拷贝赋值运算符的约束对于拷贝构造函数是不适用的:

std::string newDog("Persephone");
NamedObject<int> p(newDog, 2);
NamedObject<int> p2(p);	//@ 调用编译器默认生成的拷贝构造函数

总结

  • 编译器可以隐式生成一个类的 default 构造函数,拷贝构造函数,拷贝赋值运算符和析构函数。
  • 默认生成的析构函数是非虚析构函数,除非该类继承自一个基类,且基类中含有虚析构函数。
  • 拷贝构造函数与拷贝赋值运算符,都是简单地从源对象拷贝每一个非静态数据成员到目标对象。
  • 含有引用成员,const 成员的类,编译器不会默认生成拷贝赋值运算符,需要自己定义,但是这种约束对于拷贝构造函数则无限制。
  • 如果基类将拷贝赋值运算符声明为 private,编译器将不会为从它继承的派生类生成隐式拷贝赋值运算符。